import type { NextConfigComplete } from '../server/config-shared'
import loadConfig from '../server/config'
import * as Log from '../build/output/log'

const unsupportedTurbopackNextConfigOptions = [
  // Left to be implemented (priority)
  // 'experimental.clientRouterFilter',
  // 'experimental.optimizePackageImports',
  // 'compiler.emotion',
  // 'compiler.reactRemoveProperties',
  // 'compiler.relay',
  // 'compiler.removeConsole',
  // 'compiler.styledComponents',
  'experimental.fetchCacheKeyPrefix',

  // Left to be implemented
  // 'excludeDefaultMomentLocales',
  // 'experimental.optimizeServerReact',
  'experimental.clientRouterFilterAllowedRate',
  // 'experimental.serverMinification',
  // 'experimental.serverSourceMaps',

  'experimental.allowedRevalidateHeaderKeys',
  'experimental.extensionAlias',
  'experimental.fallbackNodePolyfills',

  'experimental.sri.algorithm',
  'experimental.swcTraceProfiling',

  // Left to be implemented (Might not be needed for Turbopack)
  'experimental.craCompat',
  'experimental.disablePostcssPresetEnv',
  'experimental.esmExternals',
  // This is used to force swc-loader to run regardless of finding Babel.
  'experimental.forceSwcTransforms',
  'experimental.fullySpecified',
  'experimental.urlImports',
  'experimental.slowModuleDetection',
]

/**  */
export async function validateTurboNextConfig({
  dir,
  configPhase,
}: {
  dir: string
  configPhase: Parameters<typeof loadConfig>[0]
}) {
  const { defaultConfig } =
    require('../server/config-shared') as typeof import('../server/config-shared')
  const { cyan, red, underline } =
    require('../lib/picocolors') as typeof import('../lib/picocolors')
  const { interopDefault } =
    require('../lib/interop-default') as typeof import('../lib/interop-default')

  let unsupportedParts = ''

  let hasWebpackConfig = false
  let hasTurboConfig = false

  const unsupportedConfig: string[] = []
  let rawNextConfig: NextConfigComplete = {} as NextConfigComplete

  try {
    rawNextConfig = interopDefault(
      await loadConfig(configPhase, dir, {
        rawConfig: true,
      })
    )

    if (typeof rawNextConfig === 'function') {
      rawNextConfig = (rawNextConfig as any)(configPhase, {
        defaultConfig,
      })
    }
    hasWebpackConfig = Boolean(rawNextConfig.webpack)
    hasTurboConfig = Boolean(rawNextConfig.turbopack)

    const flattenKeys = (obj: any, prefix: string = ''): string[] => {
      let keys: string[] = []

      for (const key in obj) {
        const value = obj?.[key]
        if (typeof value === 'undefined') {
          continue
        }

        const pre = prefix.length ? `${prefix}.` : ''

        if (
          typeof value === 'object' &&
          !Array.isArray(value) &&
          value !== null
        ) {
          keys = keys.concat(flattenKeys(value, pre + key))
        } else {
          keys.push(pre + key)
        }
      }

      return keys
    }

    const getDeepValue = (obj: any, keys: string | string[]): any => {
      if (typeof keys === 'string') {
        keys = keys.split('.')
      }
      if (keys.length === 1) {
        return obj?.[keys?.[0]]
      }
      return getDeepValue(obj?.[keys?.[0]], keys.slice(1))
    }

    const customKeys = flattenKeys(rawNextConfig)

    for (const key of customKeys) {
      if (key.startsWith('experimental.turbo')) {
        hasTurboConfig = true
      }

      const isUnsupported =
        unsupportedTurbopackNextConfigOptions.some(
          (unsupportedKey) =>
            // Either the key matches (or is a more specific subkey) of
            // unsupportedKey, or the key is the path to a specific subkey.
            // | key     | unsupportedKey |
            // |---------|----------------|
            // | foo     | foo            |
            // | foo.bar | foo            |
            // | foo     | foo.bar        |
            key.startsWith(unsupportedKey) ||
            unsupportedKey.startsWith(`${key}.`)
        ) &&
        getDeepValue(rawNextConfig, key) !== getDeepValue(defaultConfig, key)

      if (isUnsupported) {
        unsupportedConfig.push(key)
      }
    }
  } catch (e) {
    Log.error('Unexpected error occurred while checking config', e)
  }

  // If the build was defaulted to Turbopack, we want to warn about possibly ignored webpack
  // configuration. Otherwise the user explicitly picked turbopack and thus we expect that
  // they have configured it correctly.
  if (process.env.TURBOPACK === 'auto' && hasWebpackConfig && !hasTurboConfig) {
    const configFile = rawNextConfig.configFileName ?? 'your Next config file'
    Log.error(
      `ERROR: This build is using Turbopack, with a \`webpack\` config and no \`turbopack\` config.
   This may be a mistake.

   As of Next.js 16 Turbopack is enabled by default and
   custom webpack configurations may need to be migrated to Turbopack.

   NOTE: your \`webpack\` config may have been added by a configuration plugin.

   To configure Turbopack, see https://nextjs.org/docs/app/api-reference/next-config-js/turbopack

   TIP: Many applications work fine under Turbopack with no configuration,
   if that is the case for you, you can silence this error by passing the
   \`--turbopack\` or \`--webpack\` flag explicitly or simply setting an 
   empty turbopack config in ${configFile} (e.g. \`turbopack: {}\`).`
    )

    process.exit(1)
  }

  if (unsupportedConfig.length) {
    unsupportedParts += `\n\n- Unsupported Next.js configuration option(s) (${cyan(
      'next.config.js'
    )})\n  Turbopack will ignore the following configuration options:\n${unsupportedConfig
      .map((name) => `    - ${red(name)}\n`)
      .join('')}`
  }

  if (unsupportedParts) {
    Log.error(
      `You are using configuration and/or tools that are not yet\nsupported by Next.js with Turbopack:\n${unsupportedParts}\n`
    )

    Log.warn(
      'Learn more about how to configure Turbopack with Next.js:\n' +
        underline(
          'https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopack'
        )
    )
  }

  return rawNextConfig
}
